package main

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"log"
	"net"
	"net/http"
	"strings"
	"time"

	"vov.cx/lib/env"

	"golang.org/x/crypto/acme"
	"golang.org/x/crypto/acme/autocert"
)

const (
	keyCacheDIR      = "ACM_CACHE_DIR"
	keyRenewBefore   = "ACM_RENEW_BEFORE"
	keyEmail         = "ACM_EMAIL"
	keyAllowDomain   = "ACM_ALLOW_DOMAIN"
	keyHostPolicyURL = "ACM_HOST_POLICY_URL"

	keyHttpsIp      = "HTTPS_IP"
	keyHttpsPort    = "HTTPS_PORT"
	keyHttpsEnabled = "HTTPS_ENABLED"
	keyHttpIp       = "HTTP_IP"
	keyHttpPort     = "HTTP_PORT"
)

func startWebServer(ctx context.Context, s *http.Server) {
	var closers []func(ctx context.Context) error

	go func() {
		var (
			ip      = env.GetString(keyHttpsIp, "0.0.0.0")
			port    = env.GetInt(keyHttpsPort, 443)
			enabled = env.IsTrue(keyHttpsEnabled)
		)

		if !enabled {
			return
		}

		ss := cloneHTTPServer(s)
		ss.Addr = fmt.Sprintf("%s:%d", ip, port)
		ss.TLSConfig = getCertConfig(s.TLSConfig)
		ss.RegisterOnShutdown(func() { log.Println("[HTTPS] 已结束") })
		ss.BaseContext = func(ln net.Listener) context.Context {
			log.Printf("[HTTPS] 已启动: %s\n", ln.Addr().String())
			return ctx
		}
		closers = append(closers, ss.Shutdown)
		if err := ss.ListenAndServeTLS("", ""); err != nil {
			if !errors.Is(err, http.ErrServerClosed) {
				log.Printf("[HTTPS] 已停止: %v\n", err)
				return
			}
		}
		log.Println("[HTTPS] 已停止")
	}()

	go func() {
		var (
			ip   = env.GetString(keyHttpIp, "0.0.0.0")
			port = env.GetInt(keyHttpPort, 80)
		)

		ss := cloneHTTPServer(s)
		ss.Addr = fmt.Sprintf("%s:%d", ip, port)
		ss.RegisterOnShutdown(func() { log.Println("[HTTP] 已结束") })
		ss.BaseContext = func(ln net.Listener) context.Context {
			log.Printf("[HTTP] 已启动: %s\n", ln.Addr().String())
			return ctx
		}
		closers = append(closers, ss.Shutdown)
		if err := ss.ListenAndServe(); err != nil {
			if !errors.Is(err, http.ErrServerClosed) {
				log.Printf("[HTTP] 已停止: %v\n", err)
				return
			}
		}
		log.Println("[HTTP] 已停止")
	}()

	<-ctx.Done()
	closeCtx, cancel := context.WithTimeout(context.TODO(), time.Second*5)
	defer cancel()
	for _, closer := range closers {
		_ = closer(closeCtx)
	}
}

func cloneHTTPServer(s *http.Server) *http.Server {
	return &http.Server{
		Addr:              s.Addr,
		Handler:           s.Handler,
		TLSConfig:         s.TLSConfig,
		ReadTimeout:       s.ReadTimeout,
		ReadHeaderTimeout: s.ReadHeaderTimeout,
		WriteTimeout:      s.WriteTimeout,
		IdleTimeout:       s.IdleTimeout,
		MaxHeaderBytes:    s.MaxHeaderBytes,
		TLSNextProto:      s.TLSNextProto,
		ConnState:         s.ConnState,
		ErrorLog:          s.ErrorLog,
		BaseContext:       s.BaseContext,
		ConnContext:       s.ConnContext,
	}
}

func getCertConfig(tc *tls.Config) *tls.Config {
	acm := autocert.Manager{
		Prompt:      autocert.AcceptTOS,
		Cache:       autocert.DirCache(env.GetString(keyCacheDIR, "")),
		RenewBefore: env.GetDuration(keyRenewBefore, time.Hour*24*15),
		Email:       env.GetString(keyEmail, "wen@aiyao.me"),
		HostPolicy:  getPolicy(),
	}

	autocert.NewListener()

	if tc == nil {
		tc = acm.TLSConfig()
	} else {
		tc = tc.Clone()
		tc.NextProtos = []string{"h2", "http/1.1", acme.ALPNProto}
		tc.GetCertificate = acm.GetCertificate
	}
	tc.MinVersion = tls.VersionTLS12

	return tc
}

func getPolicy() autocert.HostPolicy {
	var (
		domains       = strings.Split(env.GetString(keyAllowDomain), ",")
		hostPolicyURL = env.GetString(keyHostPolicyURL)
	)

	var hps []autocert.HostPolicy
	if len(domains) > 0 {
		hps = append(hps, autocert.HostWhitelist(domains...))
	}

	if hostPolicyURL != "" {
		hps = append(hps, onDemand(hostPolicyURL))
	}

	if len(hps) == 0 {
		hps = append(hps, func(context.Context, string) error {
			return fmt.Errorf("not allow auto ssl")
		})
	}

	return func(ctx context.Context, host string) error {
		for _, hp := range hps {
			if err := hp(ctx, host); err != nil {
				return err
			}
		}
		return nil
	}
}

func onDemand(api string) autocert.HostPolicy {
	return func(ctx context.Context, host string) error {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, api, nil)
		if err != nil {
			return err
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}
		_ = resp.Body.Close()
		if resp.StatusCode != 200 {
			return fmt.Errorf("%s: %d", host, resp.StatusCode)
		}
		return nil
	}
}
